package cruncher;

import java.util.HashSet;

import utils.Logger;
import utils.Timer;

/**
 * 
 */

/**
 * Cruncher that uses diversification (random solution pool and fix to value strategy)
 * @author igorzar
 *
 */
public class ImprovedCruncher extends NaiveCruncher {
	IFixingStrategy fixStrategy = null;
	ISolutionPool solutionPool = null;

	/**
	 * 
	 */
	public ImprovedCruncher(IFixingStrategy fix, ISolutionPool pool) {
		fixStrategy = fix;
		this.solutionPool = pool;
		displayCPLEXlog = false;
		}

	@Override
	public Solution localSearch(LocalSearchFactors params, int runningTimeSec)
	{
		int iterTime = params.ITERATION_TIME;
		double fixingFraction = params.FIXING_FRACTION;
		int noImprovementBBCounter = 0, noImprovementPolishCounter = 0;
		boolean runPolish = false; //indicates if we need to run polish heuristics, instead of B&B
		Stats stats = new Stats();
		
		long moreTime = runningTimeSec * 1000; //msec
		Timer timer = new Timer();
		int iterations = 0;
		boolean foundBetterSolution = false;
		
		Logger.STATS(String.format("Running time: %d seconds",runningTimeSec));
		Logger.STATS(params.toString());
		
		setTimeLimitSec((int)(moreTime/1000));
		Logger.TRACE(String.format("Looking for %d feasible solutions...",params.INITIAL_SOLUTIONS_NUMBER));
		timer.Start();
		Solution current = this.getFeasibleSolution(params.INITIAL_SOLUTIONS_NUMBER);
		moreTime -= timer.Stop();
		
		if (current == null)
		{
			Logger.INFO(String.format("Didn't find a feasible solution in %d seconds", runningTimeSec));
			return null;
		}
		
		Logger.INFO(String.format("Found solution with value %f",current.getObjValue()));
		Logger.INFO(String.format("Time left %d seconds",(moreTime/1000)));
		
		solutionPool.addSolution(current);
		this.setTimeLimitSec(iterTime);
		
		//fix and improve
		while (moreTime >= 0){
			current = solutionPool.getRandom(); //get next solution
			stats.iterations = iterations;
			stats.runPolish = runPolish;
			stats.fixingFraction = fixingFraction;
			
			foundBetterSolution = false;
			
			
			try
				{
					Logger.INFO(String.format("Starting search iteration: %d ",++iterations));
					this.reset(); //reset the model to original (root) w/o fixings
					
					timer.Start();
					Solution next = null;
					
					if (!runPolish)
					{ // Run B&B on the new model 
						HashSet<Variable> fixedVars = fixStrategy.getFixedSet(current, fixingFraction);
						this.fixVariables(fixedVars);
						Logger.INFO(String.format("Modified model: fixed %d variables",fixedVars.size()));
						Logger.INFO(String.format("Adding start solution with value %f",current.getObjValue()));
						Logger.TRACE("Solving...");
						this.addMIPStart(current);
						this.setTimeLimitSec(timeLimitSec);
						next = solve();
						if (next != null) //not needed due to MIP start
						{
							Logger.INFO(String.format("Found solution with value %f",next.getObjValue()));
							
							if (next.isOptimal())
							{
								Logger.TRACE("Solution is optimal");
								
								//reduce the fixing fraction if there's no improvement of initial solution
								if (next.getObjValue() >= current.getObjValue())
								{
									noImprovementBBCounter++;
								}
							} // Optimal
							else if (getMIPRelativeGap() > params.MIP_RELATIVE_GAP_THRESHOLD) //check the gap
								{
									Logger.TRACE("Solution is not optimal, will run polish");
									runPolish = true;
								} //Relative Gap
							
							stats.mipGap = getMIPRelativeGap();
							
						} // found some solution
					}// B&B
					else
					{ // Run polish on current model
						Logger.TRACE("Polishing...");
						this.addMIPStart(current);
						next = solveWithPolish(timeLimitSec, 0, 1);
						if (next != null)
						{
							Logger.INFO(String.format("Found solution with value %f",next.getObjValue()));
							
							if (next.getObjValue() >= current.getObjValue())
							{
								noImprovementPolishCounter++;
							}
						}
						runPolish = false;
						stats.mipGap = 0.0;
					} // Polish
					
					// Add new solution to the pool, if necessary
					if (next != null)
					{
						if (next.getObjValue() < current.getObjValue())
							{
								solutionPool.replaceSolution(current, next);
								foundBetterSolution = true;
							}
					}
					
					//Decide what to do with the fixing fraction
					if (noImprovementPolishCounter + noImprovementBBCounter >= params.LOCAL_MINIMUM_THRESHOLD)
					{
						//take the majority
						if (noImprovementBBCounter >= noImprovementPolishCounter)
						{
							//decrease the fixing fraction
							fixingFraction = Math.max(0, fixingFraction - params.FIXING_FRACTION_DELTA);
						}
						else
						{
							//increase the fixing fraction
							fixingFraction = Math.min(params.FIXING_FRACTION, fixingFraction + params.FIXING_FRACTION_DELTA);
						}
						
						noImprovementPolishCounter = 0;
						noImprovementBBCounter = 0;
					}
					
					if (foundBetterSolution)
					{
						noImprovementPolishCounter = 0;
						noImprovementBBCounter = 0;
					}
					
					moreTime -= timer.Stop();
					
					//printStatsOnScreen(iterations,foundBetterSolution,runPolish,fixingFraction,
					//		solutionPool.getMin().getObjValue(),runningTimeSec-moreTime/1000,moreTime/1000);
					
					Logger.INFO(String.format("Time left %d seconds",moreTime/1000));
					//Print stats
					stats.foundIntegerSolution = foundBetterSolution;
					stats.bestInteger = solutionPool.getMin().getObjValue();
					stats.timeElapsed = runningTimeSec-moreTime/1000;
					stats.moreTime = moreTime/1000;
					printStatsOnScreen(stats);
					
					if (moreTime <= 0)
					{
						break;
					}
					else if (moreTime/1000 < iterTime)
					{
						this.setTimeLimitSec((int)moreTime/1000);
					}
				}
				catch(Exception e)
				{
					e.printStackTrace();
				}
				
		} //while moreTime
		Logger.STATS(String.format("Best solution: %f",solutionPool.getMin().getObjValue()));
		return solutionPool.getMin();
	}
	
	public static void main(String[] args) {
		if (args.length == 2) {
		    try {
				String mpsFilename = args[0];
				int timeSec = Integer.parseInt(args[1]);
				Logger.enabled = true;
				NaiveCruncher cruncher =  new ImprovedCruncher(new FixingStrategy(), new LimitedSolutionPool(5));
				cruncher.readfile(mpsFilename);
				Solution solCruncher = cruncher.localSearch(new LocalSearchFactors(), timeSec);
				cruncher.cleanup();
		    } catch (Exception e) {
		        e.printStackTrace();
		        System.exit(1);
		    }
		}
		else
		{
			System.err.println("Usage: ImprovedCruncher MPSFilename Time(sec) ");
		}

	}
}
